#include <stdio.h>
#include <errno.h>
#include <limits.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <memory.h>
#include <netinet/ip_icmp.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netdb.h>

#define PKG_SIZE 1024

char package[PKG_SIZE];
char addrBuf[1024]; /* 存储ip字符串 */
int packageSize=64;
int packageSent=0;
int packageRecv=0;
int sockfd;
int pingTimes=3;
struct timeval startTime;
struct timeval endTime;

struct sockaddr_in destaddr;
char* destaddrStr;
char* hostname;


unsigned short 
checksum(unsigned short *addr,int len)
{       int nleft=len;
    int sum=0;
    unsigned short *w=addr;
    unsigned short answer=0;
     
/*把ICMP报头二进制数据以2字节为单位累加起来*/
    while(nleft>1)
    {       sum+=*w++;
        nleft-=2;
    }
    /*若ICMP报头为奇数个字节，会剩下最后一字节。把最后一个字节视为一个2字节数据的高字节，这个2字节数据的低字节为0，继续累加*/
    if( nleft==1)
    {       *(unsigned char *)(&answer)=*(unsigned char *)w;
        sum+=answer;
    }
    sum=(sum>>16)+(sum&0xffff);
    sum+=(sum>>16);
    answer=~sum;
    return answer;
}

/**
 * 初始化icmp报文
 */
int
pack(int pack_no, char* buf) {
    struct icmphdr* icmphdr = (struct icmphdr*) buf;

    bzero(buf, packageSize);

    icmphdr->type = ICMP_ECHO;
    icmphdr->code = 0;
    icmphdr->checksum = 0;   // 稍后计算
    icmphdr->un.echo.id = htons(getpid());
    icmphdr->un.echo.sequence = htons(pack_no);

    char* data = buf + sizeof(*icmphdr);

    // 时间戳写入到包中
    struct timeval* tv = (struct timeval*)data;
    gettimeofday(tv, NULL);

    // 计算校验和
    icmphdr->checksum = checksum((unsigned short*)icmphdr, packageSize);
    return packageSize;
}

void 
tv_sub(struct timeval *out,struct timeval *in)
{       if( (out->tv_usec-=in->tv_usec)<0)
    {       --out->tv_sec;
        out->tv_usec+=1000000;
    }
    out->tv_sec-=in->tv_sec;
}

int
unpack(char* buf) {
    // 这里需要注意, buf指向的是ip报文

    unsigned short chksum;
    struct ip* ip = (struct ip*)buf;
    struct icmp* icmp = (struct icmp*)(buf+sizeof(*ip));

    //checksum
    chksum = checksum((unsigned short*)icmp, packageSize);
    if (chksum) {
        printf("expected chksum: %x\n", chksum);
        printf("actual   chksum: %x\n", icmp->icmp_cksum);
        return -1;
    }

    // 计算时间
    struct timeval tv;
    gettimeofday(&tv, NULL);

    struct timeval* recvtv = (struct timeval*)icmp->icmp_data;

    tv_sub(&tv, recvtv);
    int timedelta = tv.tv_sec*1000 + tv.tv_usec/1000;

    inet_ntop(AF_INET, &destaddr.sin_addr, addrBuf, sizeof (struct sockaddr));
    printf("%d bytes from %s: icmp_seq=%d ttl=%d time=%d ms\n",
            packageSize, 
            inet_ntoa(ip->ip_src), htons(icmp->icmp_seq),
            ip->ip_ttl, timedelta);

    return 0;
}

void
ping() {
    gettimeofday(&startTime, NULL);

    for (int i = 0 ; i < pingTimes ; i++) {
        bzero(package, packageSize);
        pack(i, package);
        if (sendto(sockfd, package, packageSize, 0,
                    (struct sockaddr*)&destaddr,
                    sizeof(destaddr)) < 0) {
            perror("sendto");
            exit(-1);
        }
        packageSent++;

        socklen_t len;
        if (recvfrom(sockfd, package, packageSize, 0,
                    (struct sockaddr*)&destaddr, &len) > 0) {
            if (unpack(package) == 0) {
                packageRecv++;
            } else {
                printf("wrong package\n");
            }
        } else {
            perror("recvfrom");
        }
        gettimeofday(&endTime, NULL);
        sleep(1);
        // 延时, 每秒发送一次
    }
}

void
summary() {
    tv_sub(&endTime, &startTime);
    long totalTime = endTime.tv_sec*1000 + endTime.tv_usec/1000;

    printf("\n--- %s ping statictics ---\n", hostname);
    printf("%d packets transmitted, %d received %d%% packet loss, time %ldms",
            packageSent, packageRecv, 100*(packageRecv-packageSent)/packageSent,
            totalTime);
}

void 
intAndTriggerSummary(int signo) {
    summary();
    exit(0);
}

void
usage(char* name) {
    printf("Usage: %s <host> [-c count]\n", name);
}

int
main(int argc, char** argv) {
    signal(SIGINT, intAndTriggerSummary);

    if (argc < 2) {
        printf("Usage: %s <host>\n", argv[0]);
        exit(-1);
    }

    if (argc == 2) {
        pingTimes = INT_MAX;
    } else if (argc == 4 && strcmp(argv[2], "-c") == 0) {
        pingTimes = strtol(argv[3], NULL, 10);
        if (errno || pingTimes <= 0) {
            usage(argv[0]);
            exit(-1);
        }
    } else {
        usage(argv[0]);
        exit(-1);
    }

    struct protoent* protocol;


    // 1. 解析参数
    hostname = argv[1];

    struct hostent* host = NULL;
    if ((host = gethostbyname(hostname)) == NULL) {
        perror("gethostbyname");
        exit(-1);
    }
    
    struct in_addr* address = (struct in_addr*)host->h_addr_list[0];
    destaddr.sin_addr = *address;
    destaddrStr = inet_ntoa(*address);

    bzero(&destaddr, sizeof(destaddr));
    destaddr.sin_addr = *address;
    destaddr.sin_family = AF_INET;

    if ((protocol = getprotobyname("icmp")) == NULL) {
        perror("getprotobyname");
        exit(-1);
    }

    // 2. 创建套接字
    if ((sockfd = socket(AF_INET, SOCK_RAW, protocol->p_proto)) < 0) {
        perror("socket");
        exit(-1);
    }
    setuid(getuid());

    printf("PING %s (%s)\n", hostname, destaddrStr);

    // 3. 发送数据
    // 开始ping
    ping();

    // 4. 统计数据
    summary();

    return 0;
}
